1. 計算量とオーダー

1.1. オーダー表現

$ n \in \mathbb{N}$

一変数で考える(簡単化)


入力のサイズ $ n $ を大きくしていったときの計算量 $ f(n) $ の振る舞いを調べる

関数 $ g(n) $ と2つの定数 $ n_0, c $が存在し, $ n \gt n_0 $ である $ n $ に対して,常に

\begin{align} f(n) \leq c g(n) \Rightarrow f(n) = O(g(n)) \end{align}

と表現し, $ f(n) $ の計算量はオーダー $ O(g(n)) $であるという.

  • 「実際にどれくらいの時間がかかるかはわかんないけど,最悪でも $ g(n) $くらいだよ」 -> 最悪計算量
  • $ g(n) $ はめちゃくちゃでかくても良いのか(とりあえずバカみたいにでかい数を返すとかでも良いのか) -> 良い(道徳的)
  • $ c $ って何,かけちゃって良いのか -> 良い ( 定数倍なんてもはや気にならないような値を $ g(n) $ はとる )

1.2. 多項式時間,指数時間アルゴリズム

$ 10^{-8} $ で1回命令を実行できるCPUを考える

1日 -> $ 24 \cdot 60 ^2 \cdot 10^{8} = 8.64 \cdot 10^{12} $ 回

  • 多項式時間
    • 肩には定数が乗ってる
    • $ n^{100} $
  • 指数時間
    • 肩に変数が乗ってる(おしまい)
    • $ 2^n $

1.3. PとNP

  • Polynomial

    • 多項式時間
  • Non-Polynomial

    • 非多項式時間

NP問題は本当に多項式時間で解けないの? -> まだ証明されていない.

2. グラフとネットワーク

2.1. グラフ

頂点と辺の集合からなる.

ここでは,辺か必ず頂点と頂点を結ぶ(自己ループはオッケー).

$ \mathcal{V} = \phi $ でも良い

  • 頂点だけでもグラフ
  • なにもなくてもグラフ
\begin{align} G = \{ \mathcal{V}, \mathcal{E} \} \end{align}

2.2. グラフのデータ表現

集合表記

\begin{align} \mathcal{G} = \{ \mathcal{V}, \mathcal{E} \} && \mathcal{V} = \{ A, B, C \} && \mathcal{E} = \{ (A, B), (A, C) \} \end{align}

隣接行列

\begin{align} \mbox{let A a Adjacency Matrix} \\ is(\mathcal{G}, \mbox{無向グラフ}) \Rightarrow is(A, \mbox{対称行列}) \end{align}\begin{align} \mathcal{G} = \left( \begin{array}{ccc} 1 & 1 & 1 \\ 1 & 0 & 0 \\ 1 & 0 & 0 \end{array} \right) \end{align}

対角要素が非負 -> 自己ループを持つ

講義では,辺は [行] から [列]

実データでは...データがでかすぎるので,グラフが作れない.

  • グラフは $ N \times N $ の行列になる

隣接リスト

\begin{align} a: (b, c, \mbox{null}) \\ b: (a, \mbox{null}) \end{align}

null: 門番

隣接行列の1つの行について1つの連結リスト

3. グラフの探索

アルゴリズム

  • 深さ優先探索
  • 幅優先探索

データ構造

  • Stack (DFSに対応)
  • Queue (BFSに対応)

3.1. 深さ優先探索 (Depth First Search)

実装

int main() {
  int StartRoom = 0;
  dfs(StartRoom);

  return 0;
}

void dfs(CurrentRoom) {
  int RoomToGo;

  // indicates we have visited <CurrentRoom>
  visited[CurrentRoom] = 1;

  for (RoomToGo = 0; RoomToGo < N; RoomToGo++) {
    if (edge[CurrentRoom][RoomToGo] != 0 &&  // We can go to <RoomToGo> from <CurrentRoom>
         visited[RoomToGo]) == 0 { // We have not visited <RoomToGo>

      dfs(RoomToGo);
    }
  }
}

計算量

  • 時間計算量: Step数 \begin{align} O(N * N) \end{align}

  • 空間計算量: 使ったメモリの数

    • N: Stackに積むやつ
    • N * N: エッジの分
\begin{align} O(N + N * N) -> O(N * N) \end{align}
ケチれないの?
  • 隣接行列に $ N * N $なんていらない

2016/05/09

3.2 幅優先探索

  • スタートから(辺の数)近い頂点を優先して探索
  • キュー
# define N 8
int edge[N][N] = {{...}, {...}, ...}

int main() {
  int StartNode = 0;
  bfs(StartNode);
  return 0;
}


void bfs(int StartNode) {
    int listed[N] = {0, 0, ...}; // みつけたらチェック
    int queue[N];
    int qhead[N];
    int qtail[N];

    int CurrentNode; // ループ変数
    int NodeToCheck; // ループ変数


    listed[StartNode] = 1;

    // enqueue
    queue[qtail++] = StartNode;

    while (qhead < qtail) {

        // dequeue
        CurrentNode = queue[qhead++];

        for (NodeToCheck = 0; NodeToCheck < N; NodeToCheck++) {

            if (edge[CurrentNode][NodeToCheck] != 0 && \
                listed[NodeToCheck] == 0) {

                // enqueue
                queue[qtail++] = NodeToCheck;
                listed[NodeToCheck] = 1;

           }
        }
    }
}

3.3. トポロジカルソート

  • 背の順に並ぶ(ユークリッド空間): これは全順序
  • 全順序

    • どの二つについても順序付けて比較可能
  • 半順序

    • 数学科の学生二人の成績は比較可能
    • 情報科学科の学生二人の成績は比較可能
    • 数学科と情報科学科の学生二人の成績は比較不可能

    • 有向グラフ

  • 探索アルゴリズムを使って実現できる.

    • 深さ優先探索
    • printfの場所を変える
int edge[8][8] = {
  {0, 1, 0, 0, 1, 0, 0, 0},
  {0, 1, 0, 0, 0, 0, 1, 0},
  {0, 0, 0, 1, 0, 0, 1, 0},
  {0, 0, 0, 0, 0, 0, 0, 1},
  {0, 0, 0, 0, 0, 1, 0, 0},
  {0, 0, 0, 0, 0, 0, 1, 0},
  {0, 0, 0, 0, 0, 0, 0, 1},
  {0, 0, 0, 0, 0, 0, 0, 0}
};


int visited[8];
int N = 8;
int CurrentNode;

void topologicalsort(int CurrentNode) {

    int NodeToGo;
    visited[CurrentNode] = 1;

    for (NodeToGo = 0; NodeToGo < N; NodeToGo++) {
        if (edge[CurrentNode][NodeToGo] != 0 && \
            visited[NodeToGo] == 0) {

                topologicalsort(NodeToGo);
        }
    }

    // ?
    printf("%d", CurrentNode);
}


int main() {

    for (int StartNode = 0; StartNode < N; StartNode++) {

        topologicalsort(StartNode);
    }
}

4. 経路問題

4.1. 最長経路問題

  • 有向グラフ中の2頂点のうち,その経路のエッジの重みが最大になる経路
  • ただし,閉路はないものとする
  1. トポロジカルソート
  2. 経路評価

4.2. 最短経路問題

  • 対象となるのは重み付きの有向グラフ(重みは正,閉路なし)
  • 2点間の最短経路とは?
同じ
  • 1 対 1
  • 1 対 N
  • N 対 N

4.2.1 ダイクストラのアルゴリズム (Dijkstra)

  • Greedy Algorithmである
    • 与えられた状況の中で(とりあえず)最良な選択を行うアルゴリズム

貪欲法は一般に全体最適なアルゴリズムではないが,ダイクストラ法は必ず全体最適に到達する例外的なアルゴリズム


# include <iostream>
# define INF 2<<10
# define N 6

int edge[N][N] = {
  {0, INF, INF, 8, 15, INF},
  {10, 0, 24, INF, 8, INF},
  {INF, INF, 0, INF, INF, 6},
  {INF, INF, INF, 0, 5, INF},
  {INF, INF, 12, INF, 0, 7},
  {INF, INF, 3, INF, INF, 0}
};

// 経路を保存する配列 (重みが正なので閉路に落ちない -> 配列のサイズはNで足りる)
int cost[N];

// 札 (これをたどっていけば最短経路がわかる)
int from[N];

// 未処理の頂点を保存する配列
int setU[N];

// setUの0からいくつまでのデータがまだ未処理である
//
// if numU = 2 then
// setU[0], setU[1] are not processed yet
//

int numU = N;

int min_node_index, min_node, min_cost;


void find_shortest_dijkstra(int StartNode) {

  for (int u = 0; u < N; u++) {
    setU[u] = u;
  }

  for (int x = 0; x < N; x++) {
    cost[x] = edge[StartNode][x];
    from[x] = StartNode;
  }

  setU[StartNode] = setU[numU-1];
  numU--;


  // 未処理のデータがまだ存在すれば繰り返し
  for (; numU > 0; numU--) {
    min_node_index = 0;
    min_node = setU[min_node_index];
    min_cost = cost[min_node];

    for (int u = 1; u < numU; u++) {
      if (cost[setU[u]] < min_cost) {
        min_node_index = u;
        min_node = setU[min_node_index];
        min_cost = cost[min_node];
      }
    }

    setU[min_node_index] = setU[numU - 1];

    for (int u = 0; u < numU - 1; u++) {
      if (edge[min_node][setU[u]] > 0 &&
          cost[setU[u]] > cost[min_node] + edge[min_node][setU[u]]) {
        cost[setU[u]] = cost[min_node] + edge[min_node][setU[u]];
        from[setU[u]] = min_node;
      }
    }
  }
}


int main() {
  find_shortest_dijkstra(0);
  std::cout << cost[4] << std::endl;

  // 最短経路の復元
  std::cout << from[4] << std::endl;
  std::cout << from[3] << std::endl;

}

4.2.2 N 対 N フロイドのアルゴリズム

  • DijkstraをN回回しても良いが...

  • 動的計画法に基づくアルゴリズム

    • 動的計画法(というか分割統治法)
      • 問題を複数の問題に分割して解き
      • 分割した問題を組み合わせることで元の問題を解く
      • 並列に問題を解くことができる
  • 並列実行できると何が良い?
    • 時間計算量を空間計算量に置き換えることができる(!!)

フロイドのアルゴリズム

最短距離を求めるアルゴリズム 経由しても良いという制約.(下にいくほど難しい)

  • 直行のみ
  • 一箇所頂点のみ経由しても良い
  • 二箇所.. ...
  • 全頂点について経由をゆる
for (int k = 0; k < N; k++) {
    for (int i = 0; i < N; i++) {
        for (int  = 0; j < N; j++) {
            d[i][j] = min(d[i][j], d[i][k] + d[k][j])
        }
    }
}

をするだけでは経路がわからない.

経路行列(N * N)を持つことで経路も表示できる.

経路行列に対して再帰呼び出しを行うと良い

計算量
  • 空間計算量: O(N^2)
  • 時間計算量: O(N^3)

ループが展開可能なので,並列に計算することで高速に求解できる.

5. マッチング問題

グラフの問題(二部グラフ) 無向グラフ

  • マッチング 二部グラフであり,エッジの属性が異なる頂点間のみに存在

  • 完全マッチング 二部グラフであり,属性ごとにN頂点が存在する エッジ数がNになる

Stable Marriagesが例として挙げられた

6. 解の探索

6.1. 全解探索

すべての可能な解を列挙する

条件を満たす解が見つかればそれで良い(運が最も悪いことを想定する)

すべての可能な数を問題のサイズ( $ N $ )で表現する

6.1.2. バックトラック

knight tour